#!/usr/bin/python
"""
cre_compile.py
"""

import collections  # for namedtuple
import re
from pprint import pprint
import string
import sys

import cre_lex
import cre_parse
import pyre

# Span is exposed for testing, etc.
__all__ = ['Regex', 'Span', 'to_ere', 'DEBUG']

# re flag distinct from re.DEBUG.  See sre_constants.py.  re.DEBUG is 128, so
# we'll skip a few and use 1024.
DEBUG = 1024


class Error(Exception):
  pass


def to_ere(cre):
  r = Regex(cre)
  return r.to_ere()


if hasattr(string, 'Template'):
  class _Template(string.Template):
    """
    This overrides the default tokenizing so that $0 can be used.
    """
    idpattern = r'[_a-z0-9]+'
else:
  _Template = None


def _CaptureDict(m):
  """Return value of capture() and iterate(pos=False)."""
  d = m.groupdict()
  # If there were no named groups, try updating groups starting at 1.
  if not d:
    d.update(enumerate(m.groups(), 1))
  d['MATCH'] = m.group(0)
  return d


def CaptureDictSpan(m):
  """Return value of capturespans() and iterate(pos=True)."""
  d = {}
  for name, value in m.groupdict().iteritems():
    c = Span(value, m.start(name), m.end(name))
    d[name] = c

  # If there were no named groups, try updating groups starting at 1.
  if not d:
    for i, value in enumerate(m.groups(), 1):
      c = Span(value, m.start(i), m.end(i))
      d[i] = c

  d['MATCH'] = Span(m.group(0), m.start(0), m.end(0))
  return d


Span = collections.namedtuple('Span', ['value', 'start', 'end'])

class Regex(object):
  """Regular expression wrapper."""

  def __init__(self, pattern, flags=0):
    """
    Args:
      pattern: regular expression in CRE syntax
      flags: regular expression flags.  Like Python's flags, but with the
          addition of annex.DEBUG
    """
    debug = False
    if flags & DEBUG:
      flags &= ~DEBUG
      debug = True

    # lex the expression
    lexer = cre_lex.GetLexer()
    tokens = list(lexer.run(pattern))

    if debug:
      print '-'*30
      print pattern
      print '-'*30
      print tokens

    # Have to allow parsing without a Start?

    # save the s-expression version
    parser = cre_parse.GetParser()
    sexpr, pos = parser.parse(tokens)

    if sexpr is None:
      raise Error("Error parsing Regex: %s" % pattern)

    _, flags_list, def_list = sexpr

    # Construct dictionary.
    defs = {}
    for definition in def_list:
      assert len(definition) == 3, definition
      _, name, value = definition
      defs[name] = value

    if debug:
      print '-'*30
      pprint(defs)

    # save the compiled Python object

    flags = pyre.convert_flags(flags_list)
    compiled = pyre.gen_re(defs, defs['Start'])
    self.re_str = flags + compiled

    # TODO:
    # We might not need this Python compiled version if we're only converting to
    # ERE, but just do it for now.  We could do it lazily in the matchers, but
    # then we have possible thread-safety issues.
    self.pattern = re.compile(self.re_str)
    self.compat = False

  def allow_compat(self):
    self.compat = True

  def _test(self, s, pos=0, endpos=None, search=True):
    """Helper for match, matchspan, capture, and capturespans."""
    if search:
      method = self.pattern.search
    else:
      method = self.pattern.match
    if endpos:
      args = (s, pos, endpos)
    else:
      args = (s, pos)
    return method(*args)

  def match(self, *args, **kwargs):
    m = self._test(*args, **kwargs)
    if not m:
      return None
    return m.group(0)

  def matchspan(self, *args, **kwargs):
    m = self._test(*args, **kwargs)
    if not m:
      return None
    return Span(m.group(0), m.start(0), m.end(0))

  def capture(self, *args, **kwargs):
    m = self._test(*args, **kwargs)
    if not m:
      return {}

    return _CaptureDict(m)

  def capturespans(self, *args, **kwargs):
    m = self._test(*args, **kwargs)
    if not m:
      return {}

    return CaptureDictSpan(m)

  def iterate(self, s, capture=False, span=False):
    """Replacement for findall()/finditer()."""
    # Function that defines the return value.
    if span:
      if capture:
        f = CaptureDictSpan
      else:
        f = lambda m: Span(m.group(0), m.start(0), m.end(0))
    else:
      if capture:
        f = _CaptureDict
      else:
        f = lambda m: m.group(0)

    i = 0
    while True:
      m = self.pattern.search(s, i)
      if not m:
        break
      item = f(m)
      yield item

      i = m.end(0)

  def execute(self, *args):
    """Low level access, good for lexers, etc."""
    return self.pattern.match(*args)

  def replace(self, *args, **kwargs):
    """Replace matches using a template or function."""
    result, _ = self.replacen(*args, **kwargs)
    return result

  # TODO: Should this use the output of match?
  def replacen(self, s, template=None, format=None, repl=None, func=None,
               count=0):
    """Like replace(), but also returns the number of matches."""
    if template:
      if _Template is None:
        raise Error('string.Template not available in this Python version')
      t = _Template(template)
      def replace(match):
        # $0 is the whole match, $1 are the numbered captures, and $name or
        # ${name} for named captures
        d = match.groupdict()
        # TODO: make this MATCH like in the dict?
        d['0'] = match.group(0)
        for i, v in enumerate(match.groups()):
          d[str(i + 1)] = v
        return t.substitute(**d)
      sub_arg = replace

    elif format:
      if not hasattr('', 'format'):
        raise Error('str.format() not available in this Python version')
      def replace(match):
        # TODO: add MATCH to groupdict?
        # {0} is the whole match, {1} are the numbered captures, and {name} for
        # named captures
        return format.format(match.group(0), *match.groups(),
                             **match.groupdict())
      sub_arg = replace

    elif repl:
      sub_arg = repl

    elif func:
      sub_arg = func

    else:
      raise Error(
          'One of template, format, repl, or func should be passed')

    return self.pattern.subn(sub_arg, s, count=count)

  # Unchanged
  def split(self, *args):
    return self.pattern.split(*args)

  def to_ere(self):
    # Take the expr, and instantiate it from a different module?  Or it make
    # make sense to weave them altogether so you can see the comparisons
    return 'TODO'

  def to_py(self):
    return self.re_str

  #
  # Obsolete methods.  Can be enabled with allow_compat().
  #

  def search(self, *args):
    """Like re.search -- not anchored on right or left."""
    if not self.compat:
      raise Error(
          "Use match() instead of search() (or import re_compat)")
    return self.pattern.search(*args)

  def sub(self, *args):
    if not self.compat:
      raise Error(
          "Use replace() instead of sub() (or import re_compat)")
    return self.pattern.sub(*args)

  def subn(self, *args):
    if not self.compat:
      raise Error(
          "Use replacen() instead of subn() (or import re_compat)")
    return self.pattern.subn(*args)

  def findall(self, *args):
    if not self.compat:
      raise Error(
          "Use iterate() instead of findall() (or import re_compat)")
    return self.pattern.findall(*args)

  def finditer(self, *args):
    if not self.compat:
      raise Error(
          "Use iterate() instead of finditer() (or import re_compat)")
    return self.pattern.finditer(*args)
